CRM 销售机会信息管理(二)3216346546

3.5 - 用户管理vxzvcfzvdfzsgbvd

在本次用户管理任务中,我们需要连接数据库实现用户的增删改查。我们可以通过用户新建页面提交新建用户并记录到数据库,通过用户列表页面获取数据库中用户的数据并展示出来,在用户编辑页中对匹配的用户内容进行编辑和修改。在本次任务中,我们推荐用 knex.js 完成数据库的交互逻辑,同时我们对这部分的数据库及逻辑操作有 MVC 代码结构的要求。

Knex.js是为 Postgres,MSSQL,MySQL,MariaDB,SQLite3,Oracle 和 Amazon Redshift设计的SQL查询构建器,其设计灵活,便于携带并且使用起来非常有趣。它具有传统的节点样式回调以及用于清洁异步流控制的承诺接口,流接口,全功能查询和模式构建器,事务支持(带保存点),连接池 以及不同查询客户和方言之间的标准化响应。Knex的主要目标环境是Node.js,您需要安装该 knex 库,然后安装适当的数据库库。

任务要求:

安装 Knex.js 连接数据库。

1
npm install -save knex mysql

knex中文文档 https://www.songxingguo.com/2018/06/30/knex.js-query/

在根目录中新建 config 文件用来存放数据库的配置信息。

1
2
3
4
5
6
7
8
9
10
const configs = {
mysql: {
host: '127.0.0.1',//地址
port: '3306',//端口
user: 'root',//连接名
password: '',//密码
database: 'mercedesbenz-crm'//数据库名
}
}
module.exports = configs

在根目录中新建 models 文件夹用户存放数据库操作相关文件。

models/knex.js 数据库配置,用于初始化mysql

1
2
3
4
5
6
7
8
9
10
11
const configs = require('../config');//引入初始化配置信息
module.exports = require('knex')({
client: 'mysql',
connection: {
host: configs.mysql.host,//把configs里的配置信息赋值到host,下同
port: configs.mysql.port,
user: configs.mysql.user,
password: configs.mysql.password,
database: configs.mysql.database,
}
})

新建 models/base.js 基础操作模型(模块化),很多数据库的操作例如简单的增删改查都可以通过继承方法来使用,封装暴露我们自定义的方法,也方便后期 knex 启用的切换不会影响到其他 models 和 controllers 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const knex = require('./knex');
class Base {
constructor(props) {
this.table = props;
}
all(){//全部
return knex(this.table).select()
}
select(params){//挑选、查找
return knex(this.table).select().where(params)
}
insert(params){//插入
return knex(this.table).insert(params)
}
update(id,params){//修改
return knex(this.table).where('id','=',id).update(params)
}

delete(id){//删除
return knex(this.table).where('id','=',id).del()
}
}
module.exports = Base

新建 models/user.js 用户模型, 继承 base

1
2
3
4
5
6
7
const Base = require('./base.js');
class User extends Base {
constructor(props = 'user') {
super(props);
}
}
module.exports = new User()

由于数据库存储的日期的数据类型为 Date 类型,因此我们需要新增一个工具函数来进行数据处理,然后在 controllers 中引入进行数据重组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}

module.exports = {
formatTime: formatTime
}

在根目录中新建 controllers 文件夹新建页面逻辑相关的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const User = require('./../models/user.js');//引入数据库模块
const { formatTime } = require('./../utils/date.js');//引入时间模块
const userController = {
insert: async function(req,res,next){//插入新一层数据
let name = req.body.name;
let phone = req.body.phone;
let password = req.body.password;
let role = req.body.role;
let created_time = new Date();
if(!name || !phone || !password || !role){
res.json({ code :0, message: '缺少参数' });
return
}
try{
const users = await User.insert({//走User里的insert模块
name, phone, password, role, created_time
});
res.json({//code:200 :表示示请求成功
code: 200,
data: users
})
}catch(e){
console.log(e);
res.json({//code:0 :表示示请求失败
code: 0,
message: '内部错误(insert)'
})
}
},
show: async function(req,res,next){//显示层
try{
const users = await User.all();//.all()全部
res.locals.users = users.map((data)=>{
data.role_display = (data.role == 1) ? '管理员':'销售';
data.created_time_display = formatTime(data.created_time)
return data
});
res.render('admin/user.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
edit: async function(req,res,next){//编辑的
try{
const id = req.params.id;
const users = await User.select({id})
res.locals.user = users[0];
res.render('admin/user_edit.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
update: async function(req,res,next) {//更改
let name = req.body.name;
let phone = req.body.phone;
let password = req.body.password;
let role = req.body.role;
let id = req.params.id;
if(!name || !phone || !password || !role){
res.json({ code: 0, message: '缺少必要参数' });
return
}
try{
const users = await User.update( id ,{
name, phone, password, role
});
res.json({
code: 200,
data: users
})
}catch(e){
console.log(e)
res.json({
code: 0,
message: '内部错误(update)'
})
}
},
delete: async function(req,res,next){//删除
let id = req.body.id;
if(!id){
res.json({ code: 0, data: '断网了' });
return
}
try{
const user = await User.delete(id);
res.json({ code: 200, data: user})
}catch(e){
res.json({ code: 0, data: e })
}
},
renderUserCreate: function(req,res,next){
res.render('admin/user_create')
}
}
module.exports = userController

在根目录中 routes 文件夹中,新建 api.js 文件用户配置 API 相关路由。

1
2
3
4
5
6
7
8
9
var express = require('express');
var router = express.Router();
var userController = require('./../controllers/user');//引入user页面逻辑相关的文件
//路径、页面逻辑层、使用insert方法
router.post('/user' , userController.insert);
router.delete('/user' , userController.delete);
router.put('/user/:id' , userController.update);

module.exports = router;

app.js引用api.js文件

1
2
3
var apiRouter = require('./routes/api');
···
app.use('/api', apiRouter);

在页面路由中,修改用户列表、用户新增、用户修改页面的路由,引用 controller 中的对应的方法。

1
2
3
4

router.get('/admin/user', userController.show);
router.get('/admin/user/create', userController.renderUserCreate);
router.get('/admin/user/:id/edit', userController.edit);

在用户新增页面,输入用户姓名、电话、密码、角色提交后数据能存进数据库,成功返回到用户列表页面。

views/admin/user_create.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{% extends './../admin_layout.tpl' %}

{% block content %}
<div class="content-title">新增人员</div>
<div class="content-control">
<a href="/admin/user">返回用户列表 >></a>
</div>
<div class="form-section">
<div class="form-item">
<input id="userName" type="text" class="form-input" placeholder="姓名"/>
</div>
<div class="form-item">
<input id="userPhone" type="text" class="form-input" placeholder="电话"/>
</div>
<div class="form-item">
<input id="userPassword" type="text" class="form-input" placeholder="密码"/>
</div>
<div class="form-item">
<select class="form-input" id="userRole">
<option value="0">请选择角色</option>
<option value="1">管理员</option>
<option value="2">销售</option>
</select>
</div>
<div class="form-item">
<button id="userSubmit" class="form-button">新增</button>
</div>
</div>
{% endblock %}


{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>//可以自行下载保存也可以使用云端
<script src="/javascripts/user_create.js"></script>
{% endblock %}

public/javascripts/user_create.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const PAGE = {
init: function() {
this.bind();
},
bind: function() {
$('#userSubmit').bind('click',this.handleSubmit);
},
handleSubmit: function() {
let name = $('#userName').val();
let phone = $('#userPhone').val();
let password = $('#userPassword').val();
let role = $('#userRole').val();
role = Number(role)

if(!name || !phone || !password || !role){
alert('请输入必要参数啊大锅');
return
}
$.ajax({
url: '/api/user',
data: { name, phone, password, role },
type: 'POST',
beforeSend: function() {
$("#userSubmit").attr("disabled",true);
},
success: function(data) {
if(data.code === 200){
alert('新增成功!')
location.href = '/admin/user'
}else{
alert(data.message)
}
},
error: function(err) {
console.log(err)
},
complete: function() {
$("#userSubmit").attr("disabled",false);
}
})
}
}
PAGE.init();

在用户列表页面,展示数据库中的所有用户信息,并生产对应的编辑地址。

views/admin/user.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{% extends './../admin_layout.tpl' %}

{% block content %}
<div class="content-title">人员管理</div>
<div class="content-control">
<a href="/admin/user/create">新增人员 >></a>
</div>
<div class="content-table">
<table class="table-container">
<tr>
<th>姓名</th>
<th>电话</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
</tr>
{% for val in users %}
<tr>
<td>{{val.name}}</td>
<td>{{val.phone}}</td>
<td>{{ val.role_display}}</td>
<td>{{ val.created_time_display}}</td>
<td>
<a href="/admin/user/{{val.id}}/edit">编辑</a>
<a class="user-del" href="javascript:;" data-id="{{val.id}}">删除</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}

{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/user_page.js"></script>
{% endblock %}

public/javascripts/user_page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//这里只有删除功能
const PAGE = {
init:function(){
this.bind();
},
bind:function(){
$('.user-del').on('click',this.userDel)
},
userDel: function() {
let id = $(this).data('id');
$.ajax({
url: '/api/user',
data: { id },
type: 'DELETE',
success: function(data) {
if(data.code === 200){
alert('删除成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
},
}
PAGE.init();

在用户编辑页面,可以修改对应的用户相关信息,成功返回到用户列表页面。

views/admin/user_edit.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{% extends './../admin_layout.tpl' %}

{% block content %}
<div class="content-title">编辑人员</div>
<div class="content-control">
<a href="/admin/user">返回用户列表 >></a>
</div>
<div class="form-section">
<div class="form-item">
<input id="userName" type="text" class="form-input" placeholder="姓名" value="{{user.name}}"/>
</div>
<div class="form-item">
<input id="userPhone" type="text" class="form-input" placeholder="电话" value="{{user.phone}}"/>
</div>
<div class="form-item">
<input id="userPassword" type="text" class="form-input" placeholder="密码" value="{{user.password}}"/>
</div>
<div class="form-item">
<select id="userRole" class="form-input">
<option value="0">请选择角色</option>
<option value="1" {% if user.role == 1 %} selected {% endif %}>管理员</option>
<option value="2" {% if user.role == 2 %} selected {% endif %}>销售</option>
</select>
</div>
<div class="form-item">
<input id="userId" type="text" hidden value="{{user.id}}" />
<button id="userSubmit" class="form-button">保存</button>
</div>
</div>
{% endblock %}


{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/user_edit.js"></script>
{% endblock %}

public/javascripts/user_edit.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const PAGE = {
init: function() {
this.bind();
},
bind: function() {
$('#userSubmit').bind('click',this.handleSubmit);
},
handleSubmit: function() {
let id = $('#userId').val();
let name = $('#userName').val();
let phone = $('#userPhone').val();
let password = $('#userPassword').val();
let role = $('#userRole').val();
role = Number(role)
if(!name || !phone || !password || !role){
alert('请输入必要参数');
return
}

$.ajax({
url: '/api/user/' + id,
data: { name, phone, password, role },
type: 'PUT',
beforeSend: function() {
$("#userSubmit").attr("disabled",true);
},
success: function(data) {
if(data.code === 200){
alert('编辑成功!')
location.href = '/admin/user'
}else{
alert(data.message)
}
},
error: function(err) {
console.log(err)
},
complete: function() {
$("#userSubmit").attr("disabled",false);
}
})
}
}

PAGE.init();

登录与退出

登陆页输入手机和密码进行登录。
服务器需要保持用户登录的状态,在下次到登陆页的时候重定向到线索页面。
服务器需要判断用户登录的状态,在未登录情况下去后台相关页面重定向到登录页。
在后台公共头部添加退出按钮,点击退出并重定向到登录页面。

1、在用户登录的 controller 方法中,获取用户提交的电话、密码,到数据库中查询是否有这个用户。如果没有,返回账户密码错误,如果有继续下一步逻辑。

controllers/auth.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const User = require('./../models/user.js');
const authCodeFunc = require('./../utils/authCode.js');

const authController = {
login:async function(req,res,next){
// 获取邮件密码参数
let phone = req.body.phone;
let password = req.body.password;
// 参数判断
if(!phone || !password){
res.json({ code: 0, data: 'params empty!' });
return
}

try{
// 通过用户模型搜索用户
const users = await User.select({ phone, password });
// 看是否有用户存在
const user = users[0];
// 如果存在
if(user){
// 将其邮箱、密码、id 组合加密
let auth_Code = phone +'\t'+ password +'\t'+ user.id +'\t'+ user.role;
auth_Code = authCodeFunc(auth_Code,'ENCODE');
// 加密防止再 cookie 中,并不让浏览器修改
res.cookie('ac', auth_Code, { maxAge: 24* 60 * 60 * 1000, httpOnly: true });
// 返回登录的信息
res.json({ code: 200, message: '登录成功!'})
}else{
res.json({ code: 0, message: '登录失败,没有此用户!' })
}
}catch(e){
res.json({ code: 0, message: '系统问题请管理员处理' })
}
},
// 渲染登录页面的模版
renderLogin:async function(req,res,next){
res.render('admin/login')
}
}

module.exports = authController;

admin/login.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{% extends './../layout.tpl' %}

{% block css %}
<link rel="stylesheet" href="/stylesheets/login.css"/>
{% endblock %}


{% block content %}
<div class="wrapper">
<div class="form-section">
<!-- <div class="form-title">管理系统后台登录</div> -->
<img class="form-title" src="https://www.mercedes-benz.com.cn/content/dam/mb-cn/footer/mercedes-benz-logo-desktop.png">
<div class="form-item">
<input id="userPhone" type="text" class="form-input" placeholder="你的手机"/>
</div>
<div class="form-item">
<input id="userPassword" type="text" class="form-input" placeholder="你的电话"/>
</div>
<div class="form-item">
<button id="userSubmit" class="form-button">内部人员登录</button>
</div>
</div>
</div>

{% endblock %}


{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/login.js"></script>
{% endblock %}

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const PAGE = {
init:function(){
this.bind();
},
bind:function(){
$('#userSubmit').on('click',this.handleSubmit)
},
handleSubmit:function(){
let phone = $('#userPhone').val();
let password = $('#userPassword').val();
$.ajax({
url:'/api/login',
data:{phone,password},
type:'POST',
beforeSend: function(){
$('#userSubmit').attr('disabled',true);
},
success: function(data){
if(data.code === 200){
alert('登陆成功啦');
location.href = '/admin/user'
}else{
alert(data.message)
}
},
error: function(err){
console.log(err)
},
complete: function(){
$('#userSubmit').attr('disabled',false);
},
})

},
}
PAGE.init();

新建一个 middleware的中间层配置,用户判断是否登录,并在路由中引入使用。如果该路由中间件判断用户未登录,即可重定向到登录页面。

filters/initFilter

1
2
3
4
5
6
7
8
module.exports = function(req,res,next) {
res.locals.seo = {
title:'MercedesBenz CRM',
keywords:'crm',
description:'mercedesbenz-crm'
}
next();
}

filter/loginFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const authCodeFunc = require('./../utils/authCode.js');
module.exports = function(req,res,next){
res.locals.isLogin = false;
res.locals.userInfo = {};
let auth_Code = req.cookies.ac;
if(auth_Code){
auth_Code = authCodeFunc(auth_Code,'DECODE');
authArr = auth_Code.split("\t");
let phone = authArr[0];
let password = authArr[1];
let id = authArr[2];
let role = authArr[3];

res.locals.isLogin = true;
res.locals.userInfo = {
phone,password,id,role
}
}
next();
}

filter/index

1
2
3
4
module.exports = function(app) {
app.use(require('./initFilter.js'));
app.use(require('./loginFilter.js'))
}

新建中间层
middlewares/suth.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const authMiddleware = {
mustLogin: function(req,res,next){
if(!res.locals.isLogin){
res.redirect('/admin/login')
return
}

next();
},

}

module.exports = authMiddleware;

在渲染登录页面的 controller 逻辑中,可以获取之前存放在 res.locals 中的数据判断用户是否登录,如果登录了,就重定向到线索管理页面。

1
2
3
4
5
6
7
8
// 渲染登录页面的模版
renderLogin:async function(req,res,next){
if(res.locals.isLogin){
res.redirect('/admin/clue')
return
}
res.render('admin/login')
}

router/index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var express = require('express');
var router = express.Router();
var userController = require('./../controllers/user.js');
var authController = require('./../controllers/auth.js');
var authMiddleware = require('./../middlewares/auth.js');//引入中间层

/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index');
});

// router.get('/admin/login', function(req, res, next) {
// res.render('admin/login');
// });
router.get('/admin/login', authController.renderLogin);
router.get('/admin/user', authMiddleware.mustLogin, userController.show);//走中间层的mustLogin方法
router.get('/admin/user/create', authMiddleware.mustLogin,userController.renderUserCreate);
router.get('/admin/user/:id/edit', authMiddleware.mustLogin,userController.edit);

router.get('/admin/clue', authMiddleware.mustLogin,function(req, res, next) {
res.render('admin/clue');
});

router.get('/admin/clue/:id',authMiddleware.mustLogin, function(req, res, next) {
res.render('admin/clue_log');
});


module.exports = router;

设置跳转模块,网页要清除cookie才能顺利退出

controllers/outlogin.js

1
2
3
4
5
6
7
8
const logoutcontroller = {
outlogin:async function(req,res,next){
res.clearCookie('ac','user_name');//清除cookie
res.clearCookie('user_name');
res.redirect('/admin/login');//设置跳转路径
}
}
module.exports = logoutcontroller;

routes/index

1
2
3
4
5
6
7
···
var logoutcontroller = require('./../controllers/outlogin.js');//引入跳转模块
/* GET home page. */
···
router.get('/admin/outlogin',logoutcontroller.outlogin);//设置路由

module.exports = router;

在公共后台加上退出选项

1
2
3
4
5
6
<div class="wrapper">
<header class="page-header">
<img class="page-header-img" src="https://www.mercedes-benz.com.cn/content/dam/mb-cn/footer/mercedes-benz-logo-desktop.png">
<div class="head-name">林熙</div>
<a href="/admin/outlogin" id="head-quit">退出</a>//输出路径/admin/outlogin
</header>

3.7 - 线索记录

落地页会在各地社交媒体中投放,例如百度推广、朋友圈广告等,拥有意向的用户会填写相关的个人信息进行预约,然后客户将会进行跟踪。本任务我们需要收集落地页中提交的信息,存放到数据库中,并在线索页面中展示。

落地页提交的信息需要包括:用户姓名、手机以及数据来源(在 URL 地址参数中获取 )

线索入库时候,需要记录服务器时间到数据库中(客户端时间会有差异性)。

后台线索列表中,提交时间需要经过格式化处理,如:2019-04-02 13:45。

落地页在获取用户姓名、手机之外还需要在获取 URL中获取来源参数,例如:http://localhost:3000?utm=baidu ,那么需要获取浏览器地址中 utm 的参数值,baidu 。

线索列表中 controller 获取到线索数据,可以 map 一次进行创建时间的 Dateformat。